/* @flow */

import type VueRouter from "./index";
import { resolvePath } from "./util/path";
import { assert, warn } from "./util/warn";
import { createRoute } from "./util/route";
import { fillParams } from "./util/params";
import { createRouteMap } from "./create-route-map";
import { normalizeLocation } from "./util/location";

export type Matcher = {
  match: (
    raw: RawLocation,
    current?: Route,
    redirectedFrom?: Location
  ) => Route,
  addRoutes: (routes: Array<RouteConfig>) => void,
};

export function createMatcher(
  routes: Array<RouteConfig>,
  router: VueRouter
): Matcher {
  const { pathList, pathMap, nameMap } = createRouteMap(routes);

  function addRoutes(routes) {
    createRouteMap(routes, pathList, pathMap, nameMap);
  }

  function match(
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {
    const location = normalizeLocation(raw, currentRoute, false, router);
    const { name } = location;

    if (name) {
      const record = nameMap[name];
      if (process.env.NODE_ENV !== "production") {
        warn(record, `Route with name '${name}' does not exist`);
      }
      if (!record) return _createRoute(null, location);
      const paramNames = record.regex.keys
        .filter((key) => !key.optional)
        .map((key) => key.name);

      if (typeof location.params !== "object") {
        location.params = {};
      }

      if (currentRoute && typeof currentRoute.params === "object") {
        for (const key in currentRoute.params) {
          if (!(key in location.params) && paramNames.indexOf(key) > -1) {
            location.params[key] = currentRoute.params[key];
          }
        }
      }

      if (record) {
        location.path = fillParams(
          record.path,
          location.params,
          `named route "${name}"`
        );
        return _createRoute(record, location, redirectedFrom);
      }
    } else if (location.path) {
      location.params = {};
      for (let i = 0; i < pathList.length; i++) {
        const path = pathList[i];
        const record = pathMap[path];
        if (matchRoute(record.regex, location.path, location.params)) {
          return _createRoute(record, location, redirectedFrom);
        }
      }
    }
    // no match
    return _createRoute(null, location);
  }

  function redirect(record: RouteRecord, location: Location): Route {
    const originalRedirect = record.redirect;
    let redirect =
      typeof originalRedirect === "function"
        ? originalRedirect(createRoute(record, location, null, router))
        : originalRedirect;

    if (typeof redirect === "string") {
      redirect = { path: redirect };
    }

    if (!redirect || typeof redirect !== "object") {
      if (process.env.NODE_ENV !== "production") {
        warn(false, `invalid redirect option: ${JSON.stringify(redirect)}`);
      }
      return _createRoute(null, location);
    }

    const re: Object = redirect;
    const { name, path } = re;
    let { query, hash, params } = location;
    query = re.hasOwnProperty("query") ? re.query : query;
    hash = re.hasOwnProperty("hash") ? re.hash : hash;
    params = re.hasOwnProperty("params") ? re.params : params;

    if (name) {
      // resolved named direct
      const targetRecord = nameMap[name];
      if (process.env.NODE_ENV !== "production") {
        assert(
          targetRecord,
          `redirect failed: named route "${name}" not found.`
        );
      }
      return match(
        {
          _normalized: true,
          name,
          query,
          hash,
          params,
        },
        undefined,
        location
      );
    } else if (path) {
      // 1. resolve relative redirect
      const rawPath = resolveRecordPath(path, record);
      // 2. resolve params
      const resolvedPath = fillParams(
        rawPath,
        params,
        `redirect route with path "${rawPath}"`
      );
      // 3. rematch with existing query and hash
      return match(
        {
          _normalized: true,
          path: resolvedPath,
          query,
          hash,
        },
        undefined,
        location
      );
    } else {
      if (process.env.NODE_ENV !== "production") {
        warn(false, `invalid redirect option: ${JSON.stringify(redirect)}`);
      }
      return _createRoute(null, location);
    }
  }

  function alias(
    record: RouteRecord,
    location: Location,
    matchAs: string
  ): Route {
    const aliasedPath = fillParams(
      matchAs,
      location.params,
      `aliased route with path "${matchAs}"`
    );
    const aliasedMatch = match({
      _normalized: true,
      path: aliasedPath,
    });
    if (aliasedMatch) {
      const matched = aliasedMatch.matched;
      const aliasedRecord = matched[matched.length - 1];
      location.params = aliasedMatch.params;
      return _createRoute(aliasedRecord, location);
    }
    return _createRoute(null, location);
  }

  function _createRoute(
    record: ?RouteRecord,
    location: Location,
    redirectedFrom?: Location
  ): Route {
    if (record && record.redirect) {
      return redirect(record, redirectedFrom || location);
    }
    if (record && record.matchAs) {
      return alias(record, location, record.matchAs);
    }
    return createRoute(record, location, redirectedFrom, router);
  }

  return {
    match,
    addRoutes,
  };
}

function matchRoute(regex: RouteRegExp, path: string, params: Object): boolean {
  const m = path.match(regex);

  if (!m) {
    return false;
  } else if (!params) {
    return true;
  }

  for (let i = 1, len = m.length; i < len; ++i) {
    const key = regex.keys[i - 1];
    const val = typeof m[i] === "string" ? decodeURIComponent(m[i]) : m[i];
    if (key) {
      params[key.name] = val;
    }
  }

  return true;
}

function resolveRecordPath(path: string, record: RouteRecord): string {
  return resolvePath(path, record.parent ? record.parent.path : "/", true);
}
